﻿using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Administration.Claims;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.WebControls;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.DirectoryServices;
using System.Linq;
using System.Security.Principal;
using System.Text.RegularExpressions;



/*
 * DO NOT directly edit G2RCP class. 
 * Instead you can inherit it and override its methods and attributes to fit your needs. 
 * Please download "G2RCP for Developers.zip" on http://G2RCP.codeplex.com to find examples and guidance.
 * */

namespace g2rcp       
{
    /// <summary>
    /// Provides search and resolution against Active Directory or another LDAP.
    /// Visit http://G2RCP.codeplex.com/ for documentation and updates.
    /// Please report any bug to http://G2RCP.codeplex.com/.
    /// Author: Yvan Duhamel
    /// Copyright (c) 2012, Yvan Duhamel. All rights reserved.
    /// </summary>
    public class G2RCP : SPClaimProvider
    {
        public const string _ProviderDisplayName = "G2RCP";
        public virtual string ProviderDisplayName
        {
            get { return _ProviderDisplayName; }
        }
        public const string _ProviderInternalName = "G2RCP";
        public virtual string ProviderInternalName
        {
            get { return _ProviderInternalName; }
        }

        public static string G2RCPUlsCategory = "G2RCP";

        public string cpt
        {
            get { return _cpt; }
            set { _cpn = value; }
        }
        private string _cpt;

        public string cpn
        {
            get { return _cpn; }
            set { _cpn = value; }
        }
        private string _cpn;

        public bool GroupsToRoles
        {
            get { return _GroupsToRoles; }
            set { _GroupsToRoles = value; }
        }
        private bool _GroupsToRoles;

        public bool SearchAndResolveGroupsToRoles
        {
            get { return _SearchAndResolveGroupsToRoles; }
            set { _SearchAndResolveGroupsToRoles = value; }
        }
        private bool _SearchAndResolveGroupsToRoles;

        public string LDAPConnectionString
        {
            get { return _LDAPConnectionString; }
            set { _LDAPConnectionString = value; }
        }
        private string _LDAPConnectionString;

        public string LDAPUserName
        {
            get { return _LDAPUserName; }
            set { _LDAPUserName = value; }
        }
        private string _LDAPUserName;

        public string LDAPPassword
        {
            get { return _LDAPPassword; }
            set { _LDAPPassword = value; }
        }
        private string _LDAPPassword;

        public bool LDAPUseSharePointDomain
        {
            get { return _LDAPUseSharePointDomain; }
            set { _LDAPUseSharePointDomain = value; }
        }
        private bool _LDAPUseSharePointDomain;

        public string LDAPfilter
        {
            get { return _LDAPfilter; }
            set { _LDAPfilter = value; }
        }
        private string _LDAPfilter;

        protected G2RCPConfig G2RCPConfig;

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="displayName"></param>
        public G2RCP(string displayName)
            : base(displayName)
        {
            // SPContext.Current is null in the STS, and there is nothing to do in the STS
            // m.cusinato 28/01/2013
            /*
            if (SPContext.Current == null)
                return;
            */

            Initialize();
        }

        /// <summary>
        /// Initializes claim provider. This method is reserved for internal use and is not intended to be called from external code or changed
        /// </summary>
        protected void Initialize()
        {
            G2RCPConfig = G2RCPConfig.GetFromConfigDB;
            if (G2RCPConfig == null)
            {
                // Failed to get it from config database, which is expected if G2RCP is inherited.
                // Create a fake persisted object just to get the default settings, it will not be saved in config database
                G2RCPConfig = G2RCPConfig.GetDefaultSettings(new G2RCPConfig());
                G2RCP.LogToULS(
                    String.Format("[{0}] Created a fake PersistedObject because it could not be found in the config database", ProviderInternalName),
                    TraceSeverity.Medium,
                    EventSeverity.Information);
            }

            this._cpt = G2RCPConfig.cpt;
            this._cpn = G2RCPConfig.cpn;
            this._GroupsToRoles = G2RCPConfig.GroupsToRoles;
            this._LDAPUseSharePointDomain = G2RCPConfig.UseSharePointDomain;
            if (!_LDAPUseSharePointDomain)
            {
                this._LDAPConnectionString = G2RCPConfig.LDAPConnectionString;
                this._LDAPUserName = G2RCPConfig.LDAPUserName;
                this._LDAPPassword = G2RCPConfig.LDAPPassword;
            }
            this._LDAPfilter = G2RCPConfig.LDAPfilter;
            this._SearchAndResolveGroupsToRoles = !String.IsNullOrEmpty(this._LDAPfilter)
                                                  &&
                                                  ( 
                                                        this._LDAPUseSharePointDomain || 
                                                        !(   
                                                            String.IsNullOrEmpty(this._LDAPConnectionString)
                                                            || 
                                                            String.IsNullOrEmpty(this._LDAPUserName)
                                                            || 
                                                            String.IsNullOrEmpty(this._LDAPPassword)
                                                        )
 
                                                  );

            SetCustomSettings();
        }
        
        /// <summary>
        /// Override this method to customize settings and behavior of the claim provider.
        /// </summary>
        protected virtual void SetCustomSettings()
        {
        }

        protected override void FillClaimsForEntity(Uri context, SPClaim entity, List<SPClaim> claims)
        {
            // m.cusinato 28/01/2013 10.00
            LogToULS(String.Format("[{0}] FillClaimsForEntity called with entity type: '{2}', value: '{1}'", ProviderDisplayName, entity.Value, entity.ClaimType), TraceSeverity.Monitorable, EventSeverity.Information);
            
            try
            {
                if (entity == null)
                    throw new ArgumentNullException("entity");

                if (claims == null)
                    throw new ArgumentNullException("claims");

                // for debugging/tracing to see if the claim provider is called
                claims.Add(
                    CreateClaim(
                        System.IdentityModel.Claims.ClaimTypes.PPID,
                        entity.Value + " - " + Guid.NewGuid().ToString() + " - " + DateTime.UtcNow.ToString(),
                        Microsoft.IdentityModel.Claims.ClaimValueTypes.String
                    )
                );

                if (_GroupsToRoles)
                {
                
                    //get the user id info - looks like:  0#.w|vbtoys\speschka
                    string userID = entity.Value;

                    //there can be more than one pipe symbol depending on the original issuer, but
                    //for windows users there will always only be one, so as long as you are just
                    //looking for the first pipe you are safe
                    int pipe = userID.IndexOf("|");

                    //now look at the character right before the first pipe; if it's a "w" then 
                    //this is a windows claim user
                    string issuerType;
                    //I should check if entity is equal to domain\user, if yes I already have what I need: done
                    if (pipe != 0)
                        issuerType = userID.Substring(pipe - 1, 1);
                    else
                    {
                        issuerType = "w";
                        pipe = -1;
                    }

                    if (issuerType == "w")
                    {
                        //get the part between the pipe and slash - that's the domain of the user
                        //that we want to use
                        int slash = userID.IndexOf("\\");

                        string domain = userID.Substring(pipe + 1, slash - pipe - 1).ToUpper();

                        //get the part after the slash, that should be the samAccountName
                        string samName = userID.Substring(slash + 1);

					    //get the length of the domain name; will be used below
					    int domainLen = domain.Length;

					    //create a Windows identity for the current user
					    WindowsIdentity wid = new WindowsIdentity(samName + "@" + domain);
                        
					    //get a reference to the groups to which the user belongs
					    IdentityReferenceCollection grps = wid.Groups;

					    //enumerate each group (which will be represented as a SID)
					    //NOTE that this includes ALL groups - builtin, local, domain, etc.
					    foreach (IdentityReference grp in grps)
					    {
						    //get the plain name of the group
						    SecurityIdentifier sid = new SecurityIdentifier(grp.Value);
						    NTAccount groupAccount = (NTAccount)sid.Translate(typeof(NTAccount));
						    string groupName = groupAccount.ToString();

						    //for domain groups, we want to remove the domain\ prefix from the group
						    //name in order to have feature parity with SAML users
                            if (groupName.Contains(domain + "\\"))
                            {
                                groupName = groupName.Substring(domainLen + 1);

                                //add the claim
                                claims.Add(
                                    new SPClaim(
                                        Microsoft.IdentityModel.Claims.ClaimTypes.Role,
                                        groupName,
                                        Microsoft.IdentityModel.Claims.ClaimValueTypes.String,
                                        SPOriginalIssuers.Format(GetOIT(), cpn)  
                                    )
                                );
                            }

                        }
				    }
                }
            }
            catch (Exception ex)
            {
                LogToULS(String.Format("[{0}] FillClaimsForEntity exception: {1}", ProviderDisplayName, ex.Message), TraceSeverity.Unexpected, EventSeverity.Warning);
            }
            
            return;
        }

        public override string Name
        {
            get { return ProviderInternalName; }
        }

        public override bool SupportsEntityInformation
        {
            // m.cusinato 28/01/2013 12.00
            get { return true; }
        }

        public override bool SupportsHierarchy
        {
            get { return false; }
        }

        public override bool SupportsResolve
        {
            get { return _SearchAndResolveGroupsToRoles; }
        }

        public override bool SupportsSearch
        {
            get { return _SearchAndResolveGroupsToRoles; }
        }

        public static void LogToULS(string message, TraceSeverity traceSeverity, EventSeverity eventSeverity)
        {
            try
            {
                SPDiagnosticsCategory category = new SPDiagnosticsCategory(G2RCPUlsCategory, traceSeverity, eventSeverity);
                SPDiagnosticsService ds = SPDiagnosticsService.Local;
                ds.WriteTrace(0, category, traceSeverity, message);
            }
            catch
            {
            }
        }

        protected override void FillClaimTypes(List<string> claimTypes)
        {
            if (claimTypes == null)
                throw new ArgumentNullException("claimTypes");

            claimTypes.Add(Microsoft.IdentityModel.Claims.ClaimTypes.PPID);
            claimTypes.Add(Microsoft.IdentityModel.Claims.ClaimTypes.Role);
        }
        
        protected override void FillClaimValueTypes(List<string> claimValueTypes)
        {
            if (claimValueTypes == null)
                throw new ArgumentNullException("claimValueTypes");

            claimValueTypes.Add(Microsoft.IdentityModel.Claims.ClaimValueTypes.String);
            claimValueTypes.Add(Microsoft.IdentityModel.Claims.ClaimValueTypes.String);
        }

        protected override void FillResolve(Uri context, string[] entityTypes,
                SPClaim resolveInput, List<PickerEntity> resolved)
        {
            FillResolve(context, entityTypes, resolveInput.Value, resolved);
        }

        protected override void FillResolve(Uri context, string[] entityTypes,
                string resolveInput, List<PickerEntity> resolved)
        {
            SearchResult[] ldsr;
            if (SearchObjectsInLDAP(String.Format(_LDAPfilter, resolveInput).Replace("*",""), out ldsr))
            {
                PickerEntity entity = GetPickerEntity(ldsr[0]);
                resolved.Add(entity);
            }
        }

        protected override void FillSearch(Uri context, string[] entityTypes, string searchPattern,
                string hierarchyNodeID, int maxCount,
            SPProviderHierarchyTree searchTree)
        {
            SearchResult[] ldsr;
            if(SearchObjectsInLDAP(String.Format(_LDAPfilter,searchPattern), out ldsr))
            {
                foreach (var user in ldsr)
                {
                    PickerEntity entity = GetPickerEntity(user);
                    searchTree.AddEntity(entity);
                }
            }
        }

        protected override void FillEntityTypes(List<string> entityTypes)
        {
            if (entityTypes == null)
                throw new ArgumentNullException("entityTypes");

            entityTypes.Add(SPClaimEntityTypes.FormsRole);
        }

        protected override void FillSchema(SPProviderSchema schema)
        {
            return;
        }
        
        protected override void FillHierarchy(Uri context, string[] entityTypes, string hierarchyNodeID, int numberOfLevels, SPProviderHierarchyTree hierarchy)
        {
            return;
        }

        //
        // A helper function to retrieve config values from the configuration file.
        //
        private object GetSearchResultPropertyFirst(SearchResult sr, string prop, object retval)
        {
            if (sr.Properties[prop] == null)
                return retval;

            if (sr.Properties[prop].Count < 1)
                return retval;

            return sr.Properties[prop][0];
        }

        private SPOriginalIssuerType GetOIT()
        {
            SPOriginalIssuerType oit;
            switch (_cpt)
            {
                case "Forms":
                    oit = SPOriginalIssuerType.Forms;
                    break;
                case "TrustedProvider":
                    oit = SPOriginalIssuerType.TrustedProvider;
                    break;
                case "ClaimProvider":
                    oit = SPOriginalIssuerType.ClaimProvider;
                    break;
                case "SecurityTokenService":
                    oit = SPOriginalIssuerType.SecurityTokenService;
                    break;
                case "Windows":
                    oit = SPOriginalIssuerType.Windows;
                    break;
                default:
                    oit = SPOriginalIssuerType.Unknown;
                    break;
            }
            return oit;
        }
        
        private PickerEntity GetPickerEntity(SearchResult user)
        {
            string mail;
            PickerEntity entity = CreatePickerEntity();
            
            entity.Claim =
                new SPClaim(
                    Microsoft.IdentityModel.Claims.ClaimTypes.Role,
                    GetSearchResultPropertyFirst(user, "sAMAccountName", "sAMAccountName missing").ToString(),
                    Microsoft.IdentityModel.Claims.ClaimValueTypes.String,
                    SPOriginalIssuers.Format(GetOIT(), cpn)
                );
            
            entity.Description = GetSearchResultPropertyFirst(user, "sAMAccountName", "sAMAccountName missing").ToString() + " (" + cpn +")";
            
            mail = GetSearchResultPropertyFirst(user, "mail", "NA").ToString();
            if (mail!="NA")
                entity.EntityData[PeopleEditorEntityDataKeys.Email] = mail;
            
            entity.DisplayText = GetSearchResultPropertyFirst(user, "cn", "cn missing").ToString();
            entity.EntityData[PeopleEditorEntityDataKeys.DisplayName] = GetSearchResultPropertyFirst(user, "displayName", "displayName missing").ToString();
            entity.EntityData[PeopleEditorEntityDataKeys.AccountName] = GetSearchResultPropertyFirst(user, "sAMAccountName", "sAMAccountName missing").ToString();
            entity.EntityType = SPClaimEntityTypes.FormsRole;
            entity.IsResolved = true;
            return entity;
        }
        
        /// <summary>
        /// Returns the DirectoryEntry to connect to LDAP
        /// </summary>
        /// <returns></returns>
        private DirectoryEntry GetDirectoryEntry()
        {
            if (!this._LDAPUseSharePointDomain)
            {
                LogToULS(String.Format("[{0}] Connect as {1} to LDAP: {2}", G2RCPUlsCategory, this._LDAPUserName, this._LDAPConnectionString), TraceSeverity.Medium, EventSeverity.Information);

                return new DirectoryEntry(
                    this._LDAPConnectionString,
                    this._LDAPUserName,
                    this._LDAPPassword);
            }
            else
            {
                LogToULS(String.Format("[{0}] Connect to AD this server is member of, with application pool credentials", G2RCPUlsCategory), TraceSeverity.Medium, EventSeverity.Information);
                return System.DirectoryServices.ActiveDirectory.Domain.GetComputerDomain().GetDirectoryEntry();
            }
        }

        /// <summary>
        /// Query LDAP to find matches
        /// </summary>
        /// <param name="filter">looks like "(&(objectClass=user) ( |(cn=value) (mail=value)))"</param>
        /// <returns>true if 1 or more objects are found</returns>
        private bool SearchObjectsInLDAP(string filter, out SearchResult[] LDAPSearchResults)
        {

            LDAPSearchResults = null;

            DirectoryEntry de = GetDirectoryEntry();
            DirectorySearcher ds = new DirectorySearcher();
            ds.SearchRoot = de;

            ds.Filter = filter;

            LogToULS(String.Format("[{0}] LDAP connection with filter: {1}.", G2RCPUlsCategory, ds.Filter), TraceSeverity.Verbose, EventSeverity.Verbose);

            ds.PropertiesToLoad.Add("objectClass");
            ds.PropertiesToLoad.Add("objectSid");
            ds.PropertiesToLoad.Add("cn");
            ds.PropertiesToLoad.Add("sAMAccountName");
            ds.PropertiesToLoad.Add("msDS-PrincipalName");
            ds.PropertiesToLoad.Add("mail");
            ds.PropertiesToLoad.Add("displayName");
            ds.PropertiesToLoad.Add("description");
            
            int nbResult = 0;
            try
            {
                SearchResultCollection collection = ds.FindAll();
                // LDAPSearchResults.Count will throw an ArgumentException if the filter is invalid
                LDAPSearchResults = new SearchResult[collection.Count];
                collection.CopyTo(LDAPSearchResults, 0);
                nbResult = LDAPSearchResults.Length;
                collection.Dispose();
            }
            catch (Exception ex)
            {
                LogToULS(String.Format("[{0}] Error during LDAP connection: {1}. Callstack: {2}", G2RCPUlsCategory, ex.Message, ex.StackTrace), TraceSeverity.Unexpected, EventSeverity.Error);
                return false;
            }
            if (null == LDAPSearchResults)
                return false;
            else
            {
                LogToULS(String.Format("[{0}] LDAP connection with filter: {1} returned {2} results.", G2RCPUlsCategory, ds.Filter, LDAPSearchResults.Length), TraceSeverity.Verbose, EventSeverity.Verbose);
                return LDAPSearchResults.Length > 0;
            }
        }

    }
}
